昨天 Day17 我們用 @CacheEvict(allEntries = true) 這招,直接把所有快取一次清空,雖然簡單粗暴,但其實很沒效率。
為什麼?因為只要有一個活動被更新,所有人的快取都會被清掉。
這感覺就像是段考完公布成績,小明數學只考了八分,結果他媽媽來學校打他,順便連你一起打。
太不合理了! 所以今天 Day18,我們要來優化這個快取系統,讓它更聰明、更省資源!
在 Spring 生態系,要解決快取問題大致上可以分成三種:
自訂 ConcurrentMapCacheManager
Spring Boot 內建的快取實作,背後其實就是一個 ConcurrentHashMap。簡單直接,不需要額外套件。
CaffeineCacheManager
一個高效能的本地快取庫,支援 TTL、LRU 淘汰、統計監控等功能。Spring Boot 原生支援,設定起來很直覺,效能比 ConcurrentMapCacheManager 好。
Redis
這是最常見的分散式快取解法,可以跨多台伺服器共用,功能完整,甚至能持久化資料,是微服架構下很熱門的快取解決方案。不過需要額外安裝 Redis server,維運成本相對較高。
簡單比較表
方案 | TTL | LRU | 跨機 | 精確刪除 | 複雜度 | 適合階段 |
---|---|---|---|---|---|---|
ConcurrentMapCache | ✗ | ✗ | ✗ | △ | 低 | 開發/測試 |
CaffeineCache | ✓ | ✓ | ✗ | ✓ | 低 | 小型專案 |
Redis | ✓ | ✓ | ✓ | ✓ | 中高 | 大型/分散式 |
為什麼這階段選 Caffeine?
- 專案還沒到分散式規模,先用 Caffeine 享受高效能、低複雜度,等未來真的需要再升級 Redis!
註:
LRU = Least Recently Used(最近最少使用) ⇒ 快取空間快滿了,優先移除最久沒被使用的資料。
例子:快取容量是3,此時有三個快取:A、B、C存入 ⇒ 此時有個D要進來 ⇒ 刪掉最久沒用的A,如此快取裡就會是:B、C、D。
TTL =Time-To-Live(有效期限),就是設置有效期限,過期就清掉。
<!-- pom.xml -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
為什麼? ⇒ 預設的 ConcurrentMapCacheManager 沒有 TTL、LRU 等功能,得透過實現CaffeineCacheManager ,Spring 才知道你想要的快取是什麼樣子。
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES) // 寫入後 5 分鐘自動過期
.expireAfterAccess(3, TimeUnit.MINUTES) // 最後訪問後 3 分鐘過期
.maximumSize(1000) // 最多 1000 筆,超過自動 LRU 淘汰
.recordStats()); // 啟用快取統計
cacheManager.setCacheNames(List.of(
"activityDistribution", "activityTrend", "activityKPIs"
));
return cacheManager;
}
}
效果
因為@CacheEvict(allEntries = true) 把所有快取都清掉,太粗暴。
所以我們要做到「只清除特定 activityId 相關的快取」,不影響其他資料。
@Service
@RequiredArgsConstructor
@Slf4j
public class CacheEvictionService {
private final CacheManager cacheManager;
// 精確清除某個 activityId 相關的所有快取
public void evictByActivityId(UUID activityId) {
String pattern = activityId.toString();
String[] cacheNames = {"activityDistribution", "activityTrend", "activityKPIs"};
for (String cacheName : cacheNames) {
org.springframework.cache.Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
Object nativeCache = cache.getNativeCache();
if (nativeCache instanceof Cache) {
Cache<Object, Object> caffeineCache = (Cache<Object, Object>) nativeCache;
ConcurrentMap<Object, Object> map = caffeineCache.asMap();
for (Object key : map.keySet()) {
String keyStr = key.toString();
// 精確比對 key 格式
if (keyStr.startsWith(pattern + "_") || keyStr.contains("_" + pattern + "_")) {
caffeineCache.invalidate(key);
}
}
}
}
}
}
}
效果
ActivityService.java
public Activity updateActivity(UUID activityId, Long userId, ...) {
// ... 更新邏輯
cacheEvictionService.evictByActivityId(activityId); // 精確清除
// ... 回傳結果
}
ActivityRecordService.java
public RecordResponse createRecord(Long userId, RecordCreateRequest request) {
// ... 新增邏輯
cacheEvictionService.evictByActivityId(request.getActivityId());
// ... 回傳結果
}
目前網站規模不大,使用者不多,Caffeine 已經足夠。等未來需要分散式支援時,再考慮引入 Redis(其實是自己想玩Redis)。
感謝閱讀